// 《围棋》作者版权所有。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

package work

import (
	"bytes"
	"fmt"
	exec "internal/execabs"
	"os"
	"strings"

	"cmd/go/internal/base"
	"cmd/go/internal/cache"
	"cmd/go/internal/cfg"
	"cmd/go/internal/fsys"
	"cmd/go/internal/str"
	"cmd/internal/buildid"
)

// 构建ID 
// 
// Go包和二进制文件都印有构建ID，这些ID记录了
// 操作ID，它是生成
// 包或二进制文件的操作的输入的散列，以及内容ID，它是操作
// 输出的散列，即归档文件或二进制文件本身。hash与build artifact cache（参见cmd/go/internal/cache）使用的hash相同，但是当存储在包和二进制文件中时，hash会被截断，因为完整长度不是需要的，而且有点笨拙。确切的形式是
// 
// actionID/[…/]contentID 
// 
// ，其中actionID和contentID由buildid准备。下面是HashToString。
// 通过查找第一个或最后一个斜杠找到。
// 通常buildID只是actionID/contentID，但请参见下面的
// 异常。
// 
// 构建ID有两个主要用途。
// 
// 1。action ID half允许安装的软件包和二进制文件充当
// 单元素缓存项。如果我们想建立数学。a带有给定的
// 操作ID中汇总的一组输入，以及安装的数学。一个已经
// 有这个动作ID，我们可以重用安装的math。而不是重建它。
// 
// 2。content ID half允许为使用特定包或二进制文件的步骤
// 轻松准备操作ID。给定操作的每个
// 输入文件的内容哈希必须包含在操作ID哈希中。
// 将内容ID存储在构建ID中，可以使用最小I/O从文件中读取内容ID，而不是读取和散列整个文件。
// 这尤其有效，因为包和二进制文件通常是动作的最大输入。
// 
// 分离操作ID和内容ID对于可复制的构建非常重要。
// 编译器是自己编译的。如果在构建过程的下一步计算
// 自己的操作ID（而不是内容ID）表示，那么编译器永远无法将其
// 的操作ID时，输出由其
// 自己的输入操作ID作为其输出操作ID（除了奇迹般的哈希冲突）。
// 相反，我们使用内容ID来计算下一个操作ID，因为
// 内容ID收敛，所以操作ID也收敛，因此
// 构建ID和整个编译器二进制文件。请参阅cmd/dist的cmdbootstrap 
// 了解实际的收敛序列。
// 
// 对于已安装的
// 二进制文件，“单元素缓存”的用途要复杂一些。对于像cmd/gofmt这样的二进制文件，有两个步骤：编译
// cmd/gofmt/*。进入主通道。a、 然后连接main。a转换成gofmt二进制文件。
// 我们不安装gofmt的主设备。a、 只有gofmt二进制文件。能够判断gofmt二进制文件是最新的意味着计算gofmt二进制文件的最终链接的操作ID 
// 并将其与已经安装的gofmt二进制文件
// 进行比较。但是计算链接
// 的操作ID意味着知道main的内容ID。a、 我们没有保留。为了避开这个问题，每个二进制文件实际上都存储了一个扩展的构建ID:
// 
// actionID（binary）/actionID（main.a）/contentID（main.a）/contentID（binary）
// 
// （注意，这可以等效为：
// 
// actionID（binary）/buildID（main.a）/contentID（binary）
// 
// 
// 在构建过程中，当构建main时。a、 gofmt二进制文件包含决定最终链接是否会产生
// 信息：如果main的操作ID为。a的输入匹配，然后
// 相同二进制文件所需的
// 在假定给定的main时，链接步骤的操作ID匹配。
// 内容ID，则二进制文件作为一个整体是最新的，无需重建。
// 
// 这一切都有点复杂，一旦我们可以依赖
// 主缓存，但至少在一开始，我们将使用基于内容的
// 陈旧性确定，在通常安装的
// 包和二进制位置之外没有缓存。

const buildIDSeparator = "/"

// actionID返回生成ID的操作ID的一半。
func actionID(buildID string) string {
	i := strings.Index(buildID, buildIDSeparator)
	if i < 0 {
		return buildID
	}
	return buildID[:i]
}

// contentID返回生成ID的内容ID的一半。
func contentID(buildID string) string {
	return buildID[strings.LastIndex(buildID, buildIDSeparator)+1:]
}

// toolID返回用于命名工具（asm、编译、封面、链接）的当前副本的唯一ID。
// 
// 重要的是，如果工具发生更改（例如，修复了编译器错误
// 并重新安装了编译器），toolID将返回不同的字符串，以便旧的
// 包存档看起来过时，并（使用修复的编译器）重建。
// 这建议使用工具二进制文件的内容哈希，存储在构建ID中。
// 
// 不幸的是，我们不能只打开工具二进制文件，因为该工具可能是通过-toolexec指定的包装程序调用的
// 我们不知道包装程序的功能。特别是，我们希望“-toolexec toolstash”
// 继续工作：如果“-toolexec toolstash”正在执行编译器的
// 隐藏副本，但go命令的作用就好像它将运行编译器的标准副本
// 一样。解决方案是让工具二进制文件使用所有工具现在都支持的“-V=full”标志告诉
// us它自己的构建ID。
// 然后我们知道我们得到了编译器的构建ID，该编译器将在构建期间实际运行
// 。（编译器二进制文件如何知道自己的内容哈希？
// 在标准链接步骤之后，我们使用updateBuildID将其存储在那里。）
// 
// 最后一个转折点是，我们更愿意为发布工具链创建可复制的构建。
// 应该可以从Linux或Mac或Windows本身交叉编译Windows，并逐位生成相同的二进制文件。如果影响构建ID的操作ID一半的工具ID 
// 基于内容ID 
// 那么Linux编译器二进制文件和Mac编译器二进制文件将具有不同的工具ID 
// 并因此产生具有不同操作ID的可执行文件。
// 为了避免这个问题，对于发行版，我们使用发行版本字符串，而不是编译器二进制文件内容哈希的
// 。这假设在所有不同系统上构建的所有编译器在语义上都是等价的，这当然只是真正的模块错误。（生成完全相同的可执行文件还需要不同的
// 构建设置在$GOROOT和文件名路径等细节上达成一致，但至少
// 工具ID不会使其不可能。）
func (b *Builder) toolID(name string) string {
	b.id.Lock()
	id := b.toolIDCache[name]
	b.id.Unlock()

	if id != "" {
		return id
	}

	path := base.Tool(name)
	desc := "go tool " + name

	// 特殊情况：未记录-vettool优先于常规vet，
	// 用于测试vet或提供替代分析工具。
	if name == "vet" && VetTool != "" {
		path = VetTool
		desc = VetTool
	}

	cmdline := str.StringList(cfg.BuildToolexec, path, "-V=full")
	cmd := exec.Command(cmdline[0], cmdline[1:]...)
	cmd.Env = base.AppendPWD(os.Environ(), cmd.Dir)
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		base.Fatalf("%s: %v\n%s%s", desc, err, stdout.Bytes(), stderr.Bytes())
	}

	line := stdout.String()
	f := strings.Fields(line)
	if len(f) < 3 || f[0] != name && path != VetTool || f[1] != "version" || f[2] == "devel" && !strings.HasPrefix(f[len(f)-1], "buildID=") {
		base.Fatalf("%s -V=full: unexpected output:\n\t%s", desc, line)
	}
	if f[2] == "devel" {
		// 在开发分支上，使用构建ID的内容ID部分。
		id = contentID(f[len(f)-1])
	} else {
		// 对于发行版，输出类似于：“编译版本go1.9.1 X:framepointer”。
		// 使用整条线路。
		id = strings.TrimSpace(line)
	}

	b.id.Lock()
	b.toolIDCache[name] = id
	b.id.Unlock()

	return id
}

// gccToolID返回由GCC驱动程序调用的工具要使用的唯一ID。这特别用于gccgo，但也可以用于gcc、g++、gfortran等。；这些工具都使用不同名称的GCC 
// 驱动程序。这里使用的方法也应该适用于足够新的clang版本。与toolID不同，
// name参数是要运行的程序。language参数是传递给GCC驱动程序的-x选项的输入文件的
// 类型。
// 
// 对于这些工具，我们没有-V=full选项来转储构建ID，
// 但是我们可以使用-V-####运行该工具，以可靠地获得正确的编译器
// 并对其进行哈希。这将在-toolexec在场的情况下工作。
// 
// 为了为已发布的编译器获得可复制的版本，我们通过
// 在
// /--版本输出中没有“实验性”来检测已发布的编译器，在这种情况下，我们只使用版本字符串。
func (b *Builder) gccToolID(name, language string) (string, error) {
	key := name + "." + language
	b.id.Lock()
	id := b.toolIDCache[key]
	b.id.Unlock()

	if id != "" {
		return id, nil
	}

	// 使用-####调用驱动程序以查看子命令和
	// 版本字符串。使用-x设置语言。假装
	// 在标准输入上编译一个空文件。
	cmdline := str.StringList(cfg.BuildToolexec, name, "-###", "-x", language, "-c", "-")
	cmd := exec.Command(cmdline[0], cmdline[1:]...)
	cmd.Env = base.AppendPWD(os.Environ(), cmd.Dir)
	// 强制未翻译的输出，以便我们看到字符串“version”。
	cmd.Env = append(cmd.Env, "LC_ALL=C")
	out, err := cmd.CombinedOutput()
	if err != nil {
		return "", fmt.Errorf("%s: %v; output: %q", name, err, out)
	}

	version := ""
	lines := strings.Split(string(out), "\n")
	for _, line := range lines {
		if fields := strings.Fields(line); len(fields) > 1 && fields[1] == "version" {
			version = line
			break
		}
	}
	if version == "" {
		return "", fmt.Errorf("%s: can not find version number in %q", name, out)
	}

	if !strings.Contains(version, "experimental") {
		// 这是一个版本。使用此行作为工具ID。
		id = version
	} else {
		// 这是一个开发版本。
		// 前导空格是正确的编译器。
		compiler := ""
		for _, line := range lines {
			if len(line) > 1 && line[0] == ' ' {
				compiler = line
				break
			}
		}
		if compiler == "" {
			return "", fmt.Errorf("%s: can not find compilation command in %q", name, out)
		}

		fields := strings.Fields(compiler)
		if len(fields) == 0 {
			return "", fmt.Errorf("%s: compilation command confusion %q", name, out)
		}
		exe := fields[0]
		if !strings.ContainsAny(exe, `/\`) {
			if lp, err := exec.LookPath(exe); err == nil {
				exe = lp
			}
		}
		id, err = buildid.ReadFile(exe)
		if err != nil {
			return "", err
		}

		// 如果找不到构建ID，请使用哈希。
		if id == "" {
			id = b.fileHash(exe)
		}
	}

	b.id.Lock()
	b.toolIDCache[key] = id
	b.id.Unlock()

	return id, nil
}

// 检查gccgo使用的汇编程序是否为GNU as。
func assemblerIsGas() bool {
	cmd := exec.Command(BuildToolchain.compiler(), "-print-prog-name=as")
	assembler, err := cmd.Output()
	if err == nil {
		cmd := exec.Command(strings.TrimSpace(string(assembler)), "--version")
		out, err := cmd.Output()
		return err == nil && strings.Contains(string(out), "GNU")
	} else {
		return false
	}
}

// gccgoBuildIDFile创建一个汇编程序文件，该文件在ELF文件的SHF_排除部分中记录
// 操作的生成ID，或在XCOFF文件的CSECT中记录
// 操作的生成ID。
func (b *Builder) gccgoBuildIDFile(a *Action) (string, error) {
	sfile := a.Objdir + "_buildid.s"

	var buf bytes.Buffer
	if cfg.Goos == "aix" {
		fmt.Fprintf(&buf, "\t.csect .go.buildid[XO]\n")
	} else if (cfg.Goos != "solaris" && cfg.Goos != "illumos") || assemblerIsGas() {
		fmt.Fprintf(&buf, "\t"+`.section .go.buildid,"e"`+"\n")
	} else if cfg.Goarch == "sparc" || cfg.Goarch == "sparc64" {
		fmt.Fprintf(&buf, "\t"+`.section ".go.buildid",#exclude`+"\n")
	} else { // cfg。Goarch==“386”| | cfg。Goarch==“amd64”
		fmt.Fprintf(&buf, "\t"+`.section .go.buildid,#exclude`+"\n")
	}
	fmt.Fprintf(&buf, "\t.byte ")
	for i := 0; i < len(a.buildID); i++ {
		if i > 0 {
			if i%8 == 0 {
				fmt.Fprintf(&buf, "\n\t.byte ")
			} else {
				fmt.Fprintf(&buf, ",")
			}
		}
		fmt.Fprintf(&buf, "%#02x", a.buildID[i])
	}
	fmt.Fprintf(&buf, "\n")
	if cfg.Goos != "solaris" && cfg.Goos != "illumos" && cfg.Goos != "aix" {
		secType := "@progbits"
		if cfg.Goarch == "arm" {
			secType = "%progbits"
		}
		fmt.Fprintf(&buf, "\t"+`.section .note.GNU-stack,"",%s`+"\n", secType)
		fmt.Fprintf(&buf, "\t"+`.section .note.GNU-split-stack,"",%s`+"\n", secType)
	}

	if cfg.BuildN || cfg.BuildX {
		for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) {
			b.Showcmd("", "echo '%s' >> %s", line, sfile)
		}
		if cfg.BuildN {
			return sfile, nil
		}
	}

	if err := os.WriteFile(sfile, buf.Bytes(), 0666); err != nil {
		return "", err
	}

	return sfile, nil
}

// buildID返回在给定文件中找到的生成ID。
// 如果找不到生成ID，则buildID返回文件的内容哈希。
func (b *Builder) buildID(file string) string {
	b.id.Lock()
	id := b.buildIDCache[file]
	b.id.Unlock()

	if id != "" {
		return id
	}

	id, err := buildid.ReadFile(file)
	if err != nil {
		id = b.fileHash(file)
	}

	b.id.Lock()
	b.buildIDCache[file] = id
	b.id.Unlock()

	return id
}

// fileHash返回指定文件的内容哈希。
func (b *Builder) fileHash(file string) string {
	file, _ = fsys.OverlayPath(file)
	sum, err := cache.FileHash(file)
	if err != nil {
		return ""
	}
	return buildid.HashToString(sum)
}

// useCache试图通过使用早期生成的缓存结果来满足操作a，该操作具有操作ID actionHash，
// 。目前，唯一的
// 缓存结果是目标上安装的包或二进制文件。
// 如果useCache决定可以使用缓存，它会设置a.buildID 
// 和a.builded供父操作使用，然后返回true。
// 否则，它会将a.buildID设置为临时生成ID，以便在生成中使用
// 并返回false。当useCache返回false时，预期调用方将生成目标，然后调用updateBuildID来完成
// 生成ID计算。
// 当useCache返回false时，它可能在a的工作期间启动了对输出
// 的缓冲。调用者应该推迟b.flushOutput（a），以确保不管操作
// 是否成功，最终都会调用该flushOutput。flushOutput调用必须在updateBuildID之后发生。
func (b *Builder) useCache(a *Action, actionHash cache.ActionID, target string) bool {
	// 此处构建ID的后半部分是内容哈希的占位符。
	// 重要的是，整体buildID不太可能偶然出现在输出中，但这应该由
	// actionID的一半负责；如果它也出现在输入中，就像
	// 120位部分SHA256冲突。ABCFDG 
	a.actionID = actionHash
	actionID := buildid.HashToString(actionHash)
	if a.json != nil {
		a.json.ActionID = actionID
	}
	a.buildID = actionID + buildIDSeparator + contentID

	// 请参阅上面的“构建ID”注释。
	if a.Mode == "link" {
		mainpkg := a.Deps[0]
		a.buildID = actionID + buildIDSeparator + mainpkg.buildID + buildIDSeparator + contentID
	}

	// 检查目标是否存在，是否与预期的操作ID匹配。
	// 如果是，它是最新的，我们可以重用它，而不是重建它。
	var buildID string
	if target != "" && !cfg.BuildA {
		buildID, _ = buildid.ReadFile(target)
		if strings.HasPrefix(buildID, actionID+buildIDSeparator) {
			a.buildID = buildID
			if a.json != nil {
				a.json.BuildID = a.buildID
			}
			a.built = target
			// 毒药a.目标在构建中稍后捕获使用。
			a.Target = "DO NOT USE - " + a.Mode
			return true
		}
	}

	// 构建主包的特殊情况：如果我们唯一想让包链接一个二进制文件，而二进制文件是
	// 已经是最新的，那么为了避免重建，请将包
	// 也报告为最新的。请参阅上面的“构建ID”注释。
	// TODO（rsc）：重写此代码以在链接操作上使用TryCache函数。
	if target != "" && !cfg.BuildA && !b.NeedExport && a.Mode == "build" && len(a.triggers) == 1 && a.triggers[0].Mode == "link" {
		buildID, err := buildid.ReadFile(target)
		if err == nil {
			id := strings.Split(buildID, buildIDSeparator)
			if len(id) == 4 && id[1] == actionID {
				// 暂时假设a.buildID是存储在安装的二进制文件中的包生成ID 
				// 看看这是否使
				// 即将到来的链接操作ID匹配。如果是这样，请报告
				// 我们构建了该软件包，因为
				// 链接步骤不会要求我们提供实际的软件包文件。
				// 请注意（*Builder）。LinkAction安排了所有
				// a.triggers[0]的依赖项（a除外）也是
				// a的依赖项，因此我们可以确定，
				// b.linkActionID仅访问
				// a已完成操作的构建ID。
				oldBuildID := a.buildID
				a.buildID = id[1] + buildIDSeparator + id[2]
				linkID := buildid.HashToString(b.linkActionID(a.triggers[0]))
				if id[0] == linkID {
					// 尽最大努力显示编译和链接步骤的输出。
					// 如果不起作用，就不起作用：重用缓存的二进制文件比重新打印诊断信息更重要。
					if c := cache.Default(); c != nil {
						showStdout(b, c, a.actionID, "stdout")      // 编译输出
						showStdout(b, c, a.actionID, "link-stdout") // 链接输出
					}

					// 毒药a.目标捕获在以后的构建中的使用。
					a.Target = "DO NOT USE - main build pseudo-cache Target"
					a.built = "DO NOT USE - main build pseudo-cache built"
					if a.json != nil {
						a.json.BuildID = a.buildID
					}
					return true
				}
				// 否则，恢复主版本的旧版本ID。
				a.buildID = oldBuildID
			}
		}
	}

	// 链接测试二进制文件的特殊情况：如果我们只想让二进制文件运行测试，并缓存测试结果，那么为了避免链接步骤，请将链接报告为最新。
	// 我们通过将测试结果记录在action-ID-half下的缓存中，避免了上一个特例中嵌套的build-ID问题。
	if !cfg.BuildA && len(a.triggers) == 1 && a.triggers[0].TryCache != nil && a.triggers[0].TryCache(b, a.triggers[0]) {
		// 尽最大努力显示编译和链接步骤的输出。
		// 如果不起作用，就不起作用：重复使用测试结果比重新打印诊断信息更重要。
		if c := cache.Default(); c != nil {
			showStdout(b, c, a.Deps[0].actionID, "stdout")      // 编译输出
			showStdout(b, c, a.Deps[0].actionID, "link-stdout") // 链接输出
		}

		// 毒药a.目标捕获在以后的构建中使用。
		a.Target = "DO NOT USE -  pseudo-cache Target"
		a.built = "DO NOT USE - pseudo-cache built"
		return true
	}

	if b.IsCmdList {
		// 在go list期间调用，以计算和记录过时。
		if p := a.Package; p != nil && !p.Stale {
			p.Stale = true
			if cfg.BuildA {
				p.StaleReason = "build -a flag in use"
			} else {
				p.StaleReason = "build ID mismatch"
				for _, p1 := range p.Internal.Imports {
					if p1.Stale && p1.StaleReason != "" {
						if strings.HasPrefix(p1.StaleReason, "stale dependency: ") {
							p.StaleReason = p1.StaleReason
							break
						}
						if strings.HasPrefix(p.StaleReason, "build ID mismatch") {
							p.StaleReason = "stale dependency: " + p1.ImportPath
						}
					}
				}
			}
		}

		// 无法从构建工件缓存更新a.buildID，
		// 这将影响目标
		// 依赖关系图中更高级别的buildID的计算。
	}

	// 检查构建工件缓存。
	// 为了go list的目的，我们将这个缓存中的点击视为“过时”（实际上，“过时”意味着p.Target是否是最新的），
	// 但是我们仍然乐于使用构建工件缓存的结果。
	if c := cache.Default(); c != nil {
		if !cfg.BuildA {
			if file, _, err := c.GetFile(actionHash); err == nil {
				if buildID, err := buildid.ReadFile(file); err == nil {
					if err := showStdout(b, c, a.actionID, "stdout"); err == nil {
						a.built = file
						a.Target = "DO NOT USE - using cache"
						a.buildID = buildID
						if a.json != nil {
							a.json.BuildID = a.buildID
						}
						if p := a.Package; p != nil {
							// 比解释其他东西过时更清楚。
							p.StaleReason = "not installed but available in build cache"
						}
						return true
					}
				}
			}
		}

		// 开始保存输出，以便以后写入缓存。
		a.output = []byte{}
	}

	return false
}

func showStdout(b *Builder, c *cache.Cache, actionID cache.ActionID, key string) error {
	stdout, stdoutEntry, err := c.GetBytes(cache.Subkey(actionID, key))
	if err != nil {
		return err
	}

	if len(stdout) > 0 {
		if cfg.BuildX || cfg.BuildN {
			b.Showcmd("", "%s  # internal", joinUnambiguously(str.StringList("cat", c.OutputFile(stdoutEntry.OutputID))))
		}
		if !cfg.BuildN {
			b.output.Lock()
			defer b.output.Unlock()
			b.Print(string(stdout))
		}
	}
	return nil
}

// flushOutput刷新a中排队的输出。
func (b *Builder) flushOutput(a *Action) {
	b.output.Lock()
	defer b.output.Unlock()
	b.Print(string(a.output))
	a.output = nil
}

// updateBuildID更新操作a写入的目标中的生成ID。
// 它要求为操作a调用useCache并返回false，
// 然后执行构建，并将临时
// a.buildID作为构建ID记录在生成的包或二进制文件中。
// updateBuildID计算最终的内容ID，并以二进制形式更新生成ID 
// 。
// 
// 与src/cmd/buildid/buildid保持同步。去
func (b *Builder) updateBuildID(a *Action, target string, rewrite bool) error {
	if cfg.BuildX || cfg.BuildN {
		if rewrite {
			b.Showcmd("", "%s # internal", joinUnambiguously(str.StringList(base.Tool("buildid"), "-w", target)))
		}
		if cfg.BuildN {
			return nil
		}
	}

	// 缓存compile/link的输出，即使我们不做其余的事情。
	if c := cache.Default(); c != nil {
		switch a.Mode {
		case "build":
			c.PutBytes(cache.Subkey(a.actionID, "stdout"), a.output)
		case "link":
			// 即使我们不缓存二进制文件，也要缓存链接器文本输出。
			// 我们可能会注意到，已安装的二进制文件是最新的，但仍然是
			// 想要假装运行链接器。
			// 将其存储在主程序包的操作ID 
			// 下，以便更容易找到我们仅有的时间。
			for _, a1 := range a.Deps {
				if p1 := a1.Package; p1 != nil && p1.Name == "main" {
					c.PutBytes(cache.Subkey(a1.actionID, "link-stdout"), a.output)
					break
				}
			}
		}
	}

	// 查找旧ID的出现，并计算新的基于内容的ID。
	r, err := os.Open(target)
	if err != nil {
		return err
	}
	matches, hash, err := buildid.FindAndHash(r, a.buildID, 0)
	r.Close()
	if err != nil {
		return err
	}
	newID := a.buildID[:strings.LastIndex(a.buildID, buildIDSeparator)] + buildIDSeparator + buildid.HashToString(hash)
	if len(newID) != len(a.buildID) {
		return fmt.Errorf("internal error: build ID length mismatch %q vs %q", a.buildID, newID)
	}

	// 替换为新的基于内容的ID。
	a.buildID = newID
	if a.json != nil {
		a.json.BuildID = a.buildID
	}
	if len(matches) == 0 {
		// 假设用户指定了-buildid=以覆盖我们将要选择的内容。
		return nil
	}

	if rewrite {
		w, err := os.OpenFile(target, os.O_RDWR, 0)
		if err != nil {
			return err
		}
		err = buildid.Rewrite(w, matches, newID)
		if err != nil {
			w.Close()
			return err
		}
		if err := w.Close(); err != nil {
			return err
		}
	}

	// 缓存包生成，但不是二进制文件（链接步骤）。
	// 预计二进制文件的重用频率不会像单个软件包一样高，而且它们的大小要大得多，因此二进制文件的缓存占用空间与效用比
	// 要低得多。
	// 不缓存链接步骤也确保重复的“开始运行”至少
	// 始终重新运行链接器，以便它们不会太快。
	// /（我们不希望人们认为go是一种脚本语言。）请注意，如果我们开始缓存二进制文件，那么我们将从缓存中复制二进制文件以运行它们，然后将其复制出来，这意味着go进程本身正在编写一个二进制文件，然后执行它，因此我们需要防御exec中讨论的
	// ETXTBSY问题。去戈朗。org/issue/22220。
	if c := cache.Default(); c != nil && a.Mode == "build" {
		r, err := os.Open(target)
		if err == nil {
			if a.output == nil {
				panic("internal error: a.output not set")
			}
			outputID, _, err := c.Put(a.actionID, r)
			r.Close()
			if err == nil && cfg.BuildX {
				b.Showcmd("", "%s # internal", joinUnambiguously(str.StringList("cp", target, c.OutputFile(outputID))))
			}
			if b.NeedExport {
				if err != nil {
					return err
				}
				a.Package.Export = c.OutputFile(outputID)
				a.Package.BuildID = a.buildID
			}
		}
	}

	return nil
}
